package cn.edu.dgut.css.sai.course.wechatoauth2demo.config;

import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.http.*;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.expression.*;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 实训课程：基于SpringSecurity的微信第三方账号登录
 *
 * <li>参考：https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html</li>
 *
 * @author sai
 * @see RestTemplateAutoConfiguration
 * @see RestTemplateBuilderConfigurer
 * @see HttpMessageConvertersAutoConfiguration
 * @see JacksonHttpMessageConvertersConfiguration
 * @see HttpMessageConverters
 * @see HttpMessageConverter
 * @see MappingJackson2HttpMessageConverter
 * @since 2020/12/26
 */
@SuppressWarnings("JavadocReference")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WeChatOAuth2ClientProperties.class)
public class WeChatOAuth2LoginConfiguration {

    /**
     * 自定义一个 MappingJackson2HttpMessageConverter，用于转换 微信开放平台的微信网页授权API 的响应内容。
     * 默认配置的MappingJackson2HttpMessageConverter不支持text/plain的content-type响应的转换。
     */
    @Bean
    HttpMessageConverter<Object> customHttpMessageConverter() {
        MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
        List<MediaType> mediaTypes = new ArrayList<>(Collections.singletonList(MediaType.TEXT_PLAIN));
        httpMessageConverter.setSupportedMediaTypes(mediaTypes);
        httpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        return httpMessageConverter;
    }

    @Bean
    OAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, WeChatOAuth2ClientProperties weChatOAuth2ClientProperties) {
        return new WeChatAuth2AuthorizationRequestResolver(clientRegistrationRepository, weChatOAuth2ClientProperties);
    }

    @Bean
    OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customOAuth2AccessTokenResponseClient(RestTemplateBuilder restTemplateBuilder, WeChatOAuth2ClientProperties weChatOAuth2ClientProperties) {
        return new WeChatOAuth2AccessTokenResponseClient(restTemplateBuilder, weChatOAuth2ClientProperties);
    }

    @Bean
    OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService(RestTemplateBuilder restTemplateBuilder, WeChatOAuth2ClientProperties weChatOAuth2ClientProperties) {
        return new WeChatOAuth2UserService(restTemplateBuilder, weChatOAuth2ClientProperties);
    }

    /**
     * 定义安全过滤链
     *
     * <p></p>
     * <h2>{@link OAuth2LoginConfigurer#init(HttpSecurityBuilder)}的配置源码分析备忘：</h2>
     * <li>当调用 http.oauth2Login() 时，实际是实例化了一个{@link OAuth2LoginConfigurer}对象，它的父类的默认构造函数首先会被执行，里面设置了一个默认的{@code '/login'}的登录页面uri，同时把它作为参数构建了一个{@link LoginUrlAuthenticationEntryPoint}。</li>
     * <li>如果我们的OAuth2客户端注册数大于1时，不会改变默认的{@code '/login'}的登录页面uri，默认配置回调处理的uri为'/login/oauth2/code/*'，默认的错误处理页面uri为登录页的uri+?error。同时会利用{@link DefaultLoginPageGeneratingFilter}生成OAuth2的默认登录页面。它支持多种类型的登录页面生成,如formlogin、openid等 。</li>
     * <li>如果我们的OAuth2客户端注册数等于1时，会改变默认的登录页面uri，并设置为这个注册数客户端的地址为'/oauth2/authorization'+客户端名称，同时以这个登录页面uri构造一个新的{@link LoginUrlAuthenticationEntryPoint}。默认的错识处理页面uri也同时被修改。</li>
     * <li>如果我们自定义了登录页面的uri话，{@link DefaultLoginPageGeneratingFilter}则不会生成OAuth2的默认登录页面内容。</li>
     * <li>上面的登录页面的设置，会同步到{@link ExceptionHandlingConfigurer}，即会修改{@link ExceptionTranslationFilter}的配置。</li>
     * <li>{@link LoginUrlAuthenticationEntryPoint}默认访问登录页面的策略是 <b>重定向</b> 。</li>
     *
     *
     * @see EnableWebSecurity
     * @see WebSecurityConfiguration
     * @see HttpSecurityConfiguration
     * @see HttpSecurityConfiguration#httpSecurity()
     * @see SecurityFilterChain
     * @see ExceptionHandlingConfigurer
     * @see ExceptionTranslationFilter
     * @see AccessDeniedHandler
     * @see AccessDeniedHandlerImpl
     * @see FilterSecurityInterceptor
     * @see FilterSecurityInterceptor#obtainSecurityMetadataSource()
     * @see AffirmativeBased
     * @see WebExpressionVoter
     * @see WebExpressionConfigAttribute
     * @see DefaultWebSecurityExpressionHandler
     * @see AbstractAuthenticationFilterConfigurer#registerAuthenticationEntryPoint(HttpSecurityBuilder, AuthenticationEntryPoint)
     * @see OAuth2LoginConfigurer#init(HttpSecurityBuilder)
     * @see OAuth2LoginConfigurer#configure(HttpSecurityBuilder)
     */
    @Bean
    SecurityFilterChain customSecurityFilterChain(HttpSecurity http,
                                                  OAuth2AuthorizationRequestResolver authorizationRequestResolver,
                                                  OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> oAuth2AccessTokenResponseClient,
                                                  OAuth2UserService<OAuth2UserRequest, OAuth2User> auth2UserService) throws Exception {
        // @formatter:off
        // 这个过滤链匹配所有请求的uri，即会拦截所有的请求，由安全过滤链的所有过滤器先处理(安全规则)。
        http.authorizeRequests()
                .anyRequest().authenticated();

        http.oauth2Login()
//                .loginPage("/oauth2login")
                .authorizationEndpoint()
                    .authorizationRequestResolver(authorizationRequestResolver).and()
                .tokenEndpoint()
                    .accessTokenResponseClient(oAuth2AccessTokenResponseClient).and()
                .userInfoEndpoint()
                    .userService(auth2UserService);
        // @formatter:on

//        http.exceptionHandling().accessDeniedPage("");

        return http.build();
    }
}
